/* Copyright (c) 2016-2020 Enfry Ltd. All Rights Reserved.
 * 
 * This software is the confidential and proprietary information of Enfry. 
 * You shall not disclose such Confidential Information
 * and shall use it only in accordance with the terms of the agreements
 * you entered into with Enfry.
 */
package com.enfry.common.util.httpclient;

import java.io.IOException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.enfry.common.util.Streams;
import com.enfry.common.util.constants.HttpConstants;
import com.enfry.common.util.encrypt.Ssl.DefaultHostnameVerifier;
import com.enfry.common.util.encrypt.Ssl.DefaultTrustManager;
import com.enfry.common.util.system.Systems;
import com.enfry.common.util.validate.Validates;

/**
 * Http客户端基类，实现子类实现execute功能
 * 
 * @see HttpGet GET方法调用，只需URL，无需传入参数
 * @see HttpPost POST方法调用，传入Form参数
 * @see HttpDownload GET方法调用，得到内容是大文件，保存到本地
 * @see HttpUpload POST方法调用，传入Form-data，包括文件和参数
 * @Title com.enfry.util.httpclient.HttpClient
 * @author Penghu
 * @date 2016年11月10日
 */
public abstract class HttpClient implements HttpConstants {
	public static final Pattern _REGEX_FILE_NAME_ = Pattern
			.compile("attachment;filename=\"([\\w\\-]+)\"");

	/**************************************************************************/
	// 以下为HTTP参数
	/**************************************************************************/
	protected static final Log log = LogFactory.getLog(HttpClient.class);

	private int connectTimeout = 0; // 连接超时时间，单位秒，实现传入毫秒，=0表示一直等待直到成功，!=0时时间达到则抛出java.net.SocketTimeoutException
	private int readTimeout = 0; // 读资源超时时间，单位秒，实现传入毫秒，=0表示一直等待直到成功，!=0时时间达到则抛出java.net.SocketTimeoutException

	private boolean doInput = true; // 是否输入，默认=true
	private boolean doOutput = false; // 是否输出，默认=false

	private boolean instanceFollowRedirects = false; // 当3XX重定向时，是否访问重定向的地址，HTTP默认是true，我们改为默认false，即返回location

	private boolean useCaches = false; // 设置当前是否使用缓存，我们默认=false，系统默认取defaultUseCaches=true，也可以实际设置
	private long ifModifiedSince = 0; // 设置资源比较时间
	private int chunkLength = -1; // 指定资源块大小，系统默认-1，提供了一个缺省值4096参考，如果未设置，则缓存本地再发送，否则流逐步发送，
	private int fixedContentLength = -1; // 指定读取资源内容大小，默认是未开启，按服务端ContentLength读内容大小，只有设置了才有效，默认-1

	private boolean allowUserInteraction = false; // 允许用户交互，默认=false

	private HashMap<String, String> requestPropertyMap; // 请求属性参数表
	private HashMap<String, String> responsePropertyMap; // 响应属性参数表
	private String responseStatusLine; // 响应状态行

	protected String url; // 请求连接
	private String method; // HTTP方法 [GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE]
	private X509TrustManager trustManager; // 信任管理器

	protected int responseStatus; // 响应状态码
	protected String responseText; // 响应状态信息

	public HttpClient(String url, String method) {
		this.url = url;
		this.method = method;
		this.requestPropertyMap = new HashMap<String, String>();
		this.responsePropertyMap = new HashMap<String, String>();
	}

	/***********************************************************/
	// 通用的执行方法，子类不可重写，子类重写以下三个方法
	/***********************************************************/

	public final void execute() {
		if (!isValidUrl())
			return;

		HttpURLConnection conn = null;

		try {
			// 1.预置请求连接属性和消息头信息，如果返回false表示不可执行
			if (!doPreRequestProperty())
				return;

			// 2.新建连接并连接
			conn = newHttpConnection();
			conn.connect();

			// 3.子类可重写该方法设置连接内容
			doWriteRequestContent(conn);

			// 4.获取响应状态和响应属性
			responseStatus = conn.getResponseCode();
			doReadResponseProperty(conn);

			// 5.子类可重写该方法获取连接内容
			doReadResponseContent(conn);
		} catch (SocketTimeoutException e) {
			responseStatus = _73_SOCKET_TIMEOUT_;
			responseText = "调用服务端超时";
			log.error(responseText, e);
		} catch (ConnectException e) {
			responseStatus = _91_CONNECT_EXCEPTION_;
			responseText = "连接服务器失败";
			log.error(responseText, e);
		} catch (SocketException e) {
			responseStatus = _90_SOCKET_EXCEPTION_;
			responseText = "调用服务端失败:" + e.getMessage();
			log.error(responseText, e);
		} catch (IOException e) {
			responseStatus = _98_IO_EXCEPTION_;
			responseText = "调用服务端失败:" + e.getMessage();
			log.error(responseText, e);
		} catch (Exception e) {
			responseStatus = _99_EXCEPTION_;
			responseText = "调用服务端异常:" + e.getMessage();
			log.error(responseText, e);
		} finally {
			if (conn != null)
				conn.disconnect();
		}
	}

	/***********************************************************/
	// 子类可重写的方法，包括预置请求属性、写请求内容，读响应内容
	/***********************************************************/

	/**
	 * 预处理请求属性，并检查是否可执行，默认可执行，子类重写
	 * 
	 * @return =true表示正常可执行，=false表示异常不向下执行
	 */
	protected boolean doPreRequestProperty() {
		return true;
	}

	/**
	 * 写请求内容，子类可重写
	 * 
	 * @param conn HTTP/HTTPS连接
	 * @throws IOException 可能的异常
	 */
	protected void doWriteRequestContent(HttpURLConnection conn) throws IOException {
	}

	/**
	 * 读响应内容，子类可重写
	 * 
	 * @param conn HTTP/HTTPS连接
	 * @throws IOException 可能的异常
	 */
	protected void doReadResponseContent(HttpURLConnection conn) throws IOException {
		if (responseStatus == _302_FOUND_) {// 重定向
			responseText = conn.getHeaderField("Location");
			return;
		}

		responseText = getResponseAsString(conn);
	}

	/*****************************************************************************/
	// 以下为创建连接，并设置基本属性
	/*****************************************************************************/

	private boolean isValidUrl() {
		if (Validates.isUrl(url))
			return true;

		responseStatus = _70_MALFORMED_URL_;
		responseText = "请求的URL不正确";
		return false;
	}

	/** 创建连接 */
	private HttpURLConnection newHttpConnection() throws NoSuchAlgorithmException,
			KeyManagementException, IOException {
		if (Validates.isUrlHttp(url)) {// HTTP连接
			HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
			setConnectionProperty(conn);
			return conn;
		} else {// HTTPS连接，为支持JDK1.6，设置协议为TLSv1，JDK1.7开始有TLSv1.1,TLSv1.2
			SSLContext ctx = SSLContext.getInstance(Systems.isJavaVersionMoreThen7() ? "TLSv1.2"
					: "TLSv1");
			ctx.init(null, new TrustManager[] { trustManager != null ? trustManager
					: new DefaultTrustManager() }, new SecureRandom());

			HttpsURLConnection conn = (HttpsURLConnection) new URL(url).openConnection();
			conn.setSSLSocketFactory(ctx.getSocketFactory());
			conn.setHostnameVerifier(new DefaultHostnameVerifier());
			setConnectionProperty(conn);
			return conn;
		}
	}

	/** 设置连接属性 */
	private void setConnectionProperty(HttpURLConnection conn) {
		// 方法
		try {
			conn.setRequestMethod(method);
		} catch (ProtocolException e) {
		}

		// 输入输出开关，输入默认打开，输出默认关闭
		if (!doInput)
			conn.setDoInput(doInput);
		if (doOutput)
			conn.setDoOutput(doOutput);

		// 当3XX重定向时，当前实例是否访问重定向的地址
		conn.setInstanceFollowRedirects(instanceFollowRedirects);

		// 超时设置，大于0时设置
		if (connectTimeout > 0)
			conn.setConnectTimeout(connectTimeout * 1000);
		if (readTimeout > 0)
			conn.setReadTimeout(readTimeout * 1000);

		// 是否启用缓存，默认开启
		if (!useCaches)
			conn.setUseCaches(useCaches);

		// 有设置比较资源时间
		if (ifModifiedSince > 0)
			conn.setIfModifiedSince(ifModifiedSince);

		// 有设置固定读资源内容大小，和设置资源块大小互斥，默认优先取固定读资源内容大小
		if (fixedContentLength != -1)
			conn.setFixedLengthStreamingMode(fixedContentLength);

		// 有设置资源块大小，和设置固定资源大小互斥
		if (fixedContentLength == -1 && chunkLength != -1)
			conn.setChunkedStreamingMode(chunkLength);

		// 有设置允许人工交互
		if (allowUserInteraction)
			conn.setAllowUserInteraction(allowUserInteraction);

		if (requestPropertyMap != null && !requestPropertyMap.isEmpty()) {
			for (Map.Entry<String, String> entry : requestPropertyMap.entrySet()) {
				conn.setRequestProperty(entry.getKey(), entry.getValue());
			}
		}
	}

	/***********************************************************/
	// 增加请求属性信息，包括信任管理器，属性，是否输入输出流等
	/***********************************************************/

	/** 设置请求，由请求处理 */
	public void setRequest(HttpClientRequest request) {
		request.buildRequest(this);
	}

	/** 设置信任管理器 */
	public void setTrustManager(X509TrustManager trustManager) {
		this.trustManager = trustManager;
	}

	/** 设置请求属性完成后关闭 */
	public void setConnectionClose() {
		requestPropertyMap.put(_CONNECTION_, "Close");
	}

	/** 增加请求属性 */
	public void addRequestProperty(String key, String value) {
		requestPropertyMap.put(key, value);
	}

	/** 获取请求属性表 */
	public HashMap<String, String> getRequestPropertyMap() {
		return requestPropertyMap;
	}

	/** 获取请求属性 */
	public String getRequestProperty(String key) {
		return requestPropertyMap.get(key);
	}

	/** 是否有请求属性 */
	public boolean hasRequestProperty(String key) {
		return requestPropertyMap.containsKey(key);
	}

	/** 获取配置的URL */
	public String getUrl() {
		return url;
	}

	/** 获取方法 */
	public String getMethod() {
		return method;
	}

	/** 判断是否打开了输入流 */
	public boolean isDoInput() {
		return doInput;
	}

	/** 设置是是否有输入流 */
	public void setDoInput(boolean doInput) {
		this.doInput = doInput;
	}

	/** 判断是否打开了输出流 */
	public boolean isDoOutput() {
		return doOutput;
	}

	/** 设置是是否有输出流 */
	public void setDoOutput(boolean doOutput) {
		this.doOutput = doOutput;
	}

	/** 设置连接超时时长，单位：秒 */
	public void setConnectTimeout(int connectTimeout) {
		this.connectTimeout = connectTimeout;
	}

	/** 获取连接超时时长，单位：秒 */
	public int getConnectTimeout() {
		return connectTimeout;
	}

	/** 设置数据获取超时时长，单位：秒 */
	public void setReadTimeout(int readTimeout) {
		this.readTimeout = readTimeout;
	}

	/** 获取读流超时时长 */
	public int getReadTimeout() {
		return readTimeout;
	}

	/** 设置协议是否使用缓存，默认是有条件即使用，我们修改成不使用 */
	public boolean isUseCaches() {
		return useCaches;
	}

	/** 设置协议是否使用缓存 */
	public void setUseCaches(boolean useCaches) {
		this.useCaches = useCaches;
	}

	/** 获取资源本地缓存上次最后修改时间 */
	public long getIfModifiedSince() {
		return ifModifiedSince;
	}

	/** 设置资源本地缓存上次最后修改时间 */
	public void setIfModifiedSince(long ifModifiedSince) {
		this.ifModifiedSince = ifModifiedSince;
	}

	/** 获取配置的块大小，-1表示未配置，操作时默认4096 */
	public int getChunkLength() {
		return chunkLength;
	}

	/** 设置块大小，-1表示不生效，取默认4096 */
	public void setChunkLength(int chunkLength) {
		this.chunkLength = chunkLength;
	}

	/** 获取配置的读取固定内容长度，-1表示不生效，取响应的Content-Length */
	public int getFixedContentLength() {
		return fixedContentLength;
	}

	/** 设置配置的读取固定内容长度，-1表示不生效，取响应的Content-Length */
	public void setFixedContentLength(int fixedContentLength) {
		this.fixedContentLength = fixedContentLength;
	}

	/** 判断是否允许用户交互，用于访问服务端时等待管理操作验证 */
	public boolean isAllowUserInteraction() {
		return allowUserInteraction;
	}

	/** 设置是否允许用户交互，默认是不允许 */
	public void setAllowUserInteraction(boolean allowUserInteraction) {
		this.allowUserInteraction = allowUserInteraction;
	}

	/** 判断当3XX重定向时，是否访问重定向的地址 */
	public boolean isInstanceFollowRedirects() {
		return instanceFollowRedirects;
	}

	/** 设置当3XX重定向时，是否访问重定向的地址 */
	public void setInstanceFollowRedirects(boolean instanceFollowRedirects) {
		this.instanceFollowRedirects = instanceFollowRedirects;
	}

	/***********************************************************/
	// 获取结果信息，包括状态、内容和字节流
	/***********************************************************/

	/** 增加响应属性 */
	protected void doReadResponseProperty(URLConnection conn) {
		responseStatusLine = conn.getHeaderField(0);
		Map<String, List<String>> map = conn.getHeaderFields();

		for (String key : map.keySet()) {
			if (key != null) {
				responsePropertyMap.put(key.toLowerCase(), conn.getHeaderField(key));
			}
		}
	}

	/** 获取响应状态 */
	public int getResponseStatus() {
		return responseStatus;
	}

	/** 判断响应是否成功 */
	public boolean isResponseSuccess() {
		return responseStatus == 200;
	}

	/** 获取响应内容(正确内容或错误内容) */
	public String getResponseText() {
		return responseText;
	}

	/** 返回指定的Http调用结果 */
	public HttpResult getResult() {
		return new HttpResult(responseStatus, responseText);
	}

	/** 获取响应状态行 */
	public String getResponseStatusLine() {
		return responseStatusLine;
	}

	/** 获取响应属性 */
	public String getResponseProperty(String key) {
		return responsePropertyMap.get(key.toLowerCase());
	}

	/** 是否有响应属性 */
	public boolean hasResponseProperty(String key) {
		return responsePropertyMap.containsKey(key.toLowerCase());
	}

	/** 获取响应属性表 */
	public HashMap<String, String> getResponsePropertyMap() {
		return responsePropertyMap;
	}

	/** 获取响应属性列表 */
	public List<String> getResponseList() {
		return new ArrayList<String>(responsePropertyMap.values());
	}

	/** 获取响应内容类型 */
	public String getResponseContentType() {
		return getResponseProperty(_CONTENT_TYPE_);
	}

	/** 获取响应内容字符集，默认UTF-8 */
	public String getResponseCharset() {
		return getResponseCharset(getResponseContentType());
	}

	/** 获取响应内容编码 */
	public String getResponseContentEncoding() {
		return getResponseProperty(_CONTENT_ENCODING_);
	}

	/** 判断响应内容编码是否是Gzip */
	public boolean isResponseGzip() {
		return _ENCODING_GZIP_.equals(getResponseContentEncoding());
	}

	/*****************************************************************************/
	// 以下为静态方法
	/*****************************************************************************/

	/**
	 * 判断是否响应使用GZIP压缩
	 * 
	 * @param conn HTTP连接
	 * @return =true表示启用，=false表示未启用
	 */
	public static boolean isResponseGzip(HttpURLConnection conn) {
		return "gzip".equals(conn.getContentEncoding());
	}

	/**
	 * 从响应中获取内容字符集
	 * 
	 * @param contentType 内容类型
	 * @return String,　默认UTF-8
	 */
	public static String getResponseCharset(String contentType) {

		if (Validates.isEmptyBlank(contentType))
			return _UTF_8_;

		String[] params = contentType.trim().split(";");
		for (String param : params) {
			if (!param.startsWith(_CHARSET_))
				continue;

			String[] pair = param.split("=");
			if (pair.length != 2 || Validates.isEmpty(pair[1]))
				continue;

			return pair[1];
		}

		return _UTF_8_;
	}

	/**
	 * 从响应中获取字符串，包括成功和失败的内容
	 * 
	 * @param conn HTTP连接
	 * @return 成功或失败的内容
	 * @throws IOException 可能的异常
	 */
	public static String getResponseAsString(HttpURLConnection conn) throws IOException {
		boolean isGzip = isResponseGzip(conn);
		String charset = getResponseCharset(conn.getContentType());
		if (conn.getInputStream() != null) {// 正常返回
			if (isGzip)
				return Streams.getStringGzip(conn.getInputStream(), charset);
			else
				return Streams.getString(conn.getInputStream(), charset);
		} else if (conn.getErrorStream() != null) {// 有出错返回
			String message = null;
			if (isGzip)
				message = Streams.getStringGzip(conn.getErrorStream(), charset);
			else
				message = Streams.getString(conn.getErrorStream(), charset);

			if (Validates.isEmptyBlank(message))
				message = "调用服务端接口失败，错误码：" + conn.getResponseCode();

			return message;
		} else {// 无出错内容返回
			return "调用服务端接口失败，错误码：" + conn.getResponseCode();
		}
	}

	/**
	 * 通过连接获取下载名称
	 * 
	 * @param conn HttpURLConnection
	 * @return fileName
	 */
	public static String getFileName(HttpURLConnection conn) {
		String contentDisposition = conn.getHeaderField(_CONTENT_DISPOSITION_);
		if (contentDisposition == null)
			return null;

		Matcher matcher = _REGEX_FILE_NAME_.matcher(contentDisposition);
		if (matcher.find())
			return matcher.group(1).trim();
		return null;
	}

	/**
	 * 获取上传文件时文本参数配置
	 * 
	 * @param name 参数名
	 * @param encoding 参数编码
	 * @return 参数配置说明
	 * @throws IOException
	 */
	public static byte[] getTextDisposition(String name, String encoding) throws IOException {
		StringBuilder strb = new StringBuilder().append("Content-Disposition: form-data; name=\"")
				.append(name).append("\"").append(_BR_).append(_BR_);
		return strb.toString().getBytes(encoding);
	}

	/**
	 * 获取上传文件时文件参数配置
	 * 
	 * @param name 文件参数名
	 * @param fileName 文件名
	 * @param mimeType 文件类型
	 * @param encoding 编码
	 * @return 参数配置说明
	 * @throws IOException
	 */
	public static byte[] getFileDisposition(String name, String fileName, String mimeType,
			String encoding) throws IOException {
		StringBuilder strb = new StringBuilder().append("Content-Disposition: form-data; name=\"")
				.append(name).append("\"; filename=\"").append(fileName).append("\"").append(_BR_)
				.append("Content-Type:").append(mimeType).append(_BR_).append(_BR_);
		return strb.toString().getBytes(encoding);
	}
}
